﻿////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// <copyright>Copyright 2008-2011 Andreas Huber Doenni</copyright>
// Distributed under the GNU General Public License version 2 (GPLv2).
// See accompanying file License.txt or copy at http://phuse.codeplex.com/license.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

namespace Phuse.Net.Forums
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics.CodeAnalysis;
    using System.Globalization;
    using System.IO;
    using System.Linq;
    using System.Net;
    using System.Threading;
    using System.Threading.Tasks;
    using System.Xml;

    using Phuse.Mime;

    internal delegate XmlReader CreateReader(long startIndex, long maxResults = YouTubeForumProvider.MaxResults);

    /// <summary>Provides methods to send/receive Video comments to/from the YouTube host.</summary>
    /// <threadsafety static="true" instance="false"/>
    public sealed class YouTubeForumProvider : IForumProvider<YouTubeConnection>
    {
        internal const long MaxResults = 50;
        private const string ArticleElementName = "entry";
        private long highWaterMark;

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        /// <summary>See <see cref="IForumProvider{T}.Connect"/>.</summary>
        public YouTubeConnection Connect(IHostInfo host, CancellationToken cancellationToken)
        {
            // TODO: Observe cancellationToken
            return new YouTubeConnection(host);
        }

        /// <summary>See <see cref="IForumProvider{T}.SendArticle"/>.</summary>
        public Exception SendArticle(YouTubeConnection connection, Message message)
        {
            return null;
        }

        /// <summary>See <see cref="IForumProvider{T}.GetHighWaterMark"/>.</summary>
        public long GetHighWaterMark(YouTubeConnection connection, string groupName)
        {
            using (var response = GetResponse(groupName, 1, 0))
            using (var reader = XmlReader.Create(response.GetResponseStream()))
            {
                return this.highWaterMark = GetHighWaterMark(reader);
            }
        }

        /// <summary>See <see cref="IForumProvider{T}.ReceiveHeaders"/>.</summary>
        [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", Justification = "Method is not supposed to be called by external code.")]
        public void ReceiveHeaders(
            YouTubeConnection connection,
            string groupName,
            IInboundArticleInfo lastOldArticle,
            long lastOldArticleNumber,
            SaveHeaderCallback saveHeader,
            SaveContentCallback saveContent)
        {
            var count = this.highWaterMark - lastOldArticleNumber;

            if (count <= 0)
            {
                return;
            }

            var lastOldArticleDate = lastOldArticle == null ? (DateTime?)null : lastOldArticle.DateUtc;
            var newArticle = lastOldArticleDate == null;
            var entries = this.GetEntries(groupName, lastOldArticleNumber);
            var number = this.highWaterMark - entries.Count + 1;

            foreach (var entry in entries.OrderBy(e => e.Date))
            {
                if (newArticle || (entry.Date > lastOldArticleDate))
                {
                    newArticle = true;
                    var article =
                        saveHeader(number++, entry.MessageId, entry.Date, entry.From, entry.Subject, entry.References);
                    var message =
                        new Message()
                        {
                            MessageId = entry.MessageId,
                            Date = entry.Date,
                            From = new[] { new Address(entry.From, null) },
                            Subject = entry.Subject,
                            References = entry.References,
                            Body = new TextBody(TextSubtypeNames.Plain) { Content = entry.Content }
                        };

                    using (var content = new MemoryStream())
                    {
                        message.WriteTo(content);
                        content.Position = 0;
                        saveContent(article, content);
                    }
                }
                else if ((entry.Date == lastOldArticleDate) && (lastOldArticle != null) &&
                    (entry.MessageId == lastOldArticle.MessageId))
                {
                    newArticle = true;
                }
            }
        }

        /// <summary>See <see cref="IForumProvider{T}.ReceiveContents"/>.</summary>
        public void ReceiveContents(
            YouTubeConnection connection,
            string groupName,
            IList<IInboundArticleInfo> contentsToReceive,
            SaveContentCallback saveContent,
            DeleteArticleCallback deleteArticle)
        {
            throw new NotImplementedException();
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        private static WebResponse GetResponse(string groupName, long startIndex, long maxResults)
        {
            const string QueryFormat =
                "http://gdata.youtube.com/feeds/api/videos/{0}/comments?start-index={1}&max-results={2}";
            using (var responseTask = WebRequest.Create(string.Format(
                CultureInfo.InvariantCulture, QueryFormat, groupName, startIndex, maxResults)).GetResponseAsync())
            {
                return responseTask.Result;
            }
        }

        private ICollection<Entry> GetEntries(string groupName, long lastOldArticleNumber)
        {
            // While we are retrieving comments in batches, new comments could be added such that e.g. the last comments
            // in batch number 2 are the same as the first comments in the batch no. 3 and so forth. We therefore
            // eliminate said overlaps by adding comments to a dictionary with message id as the key.
            var entries = new Dictionary<string, Entry>();
            var index = 1L;
            var count = long.MaxValue;

            while (entries.Count < count)
            {
                using (var response = GetResponse(groupName, index, MaxResults))
                using (var reader = XmlReader.Create(response.GetResponseStream()))
                {
                    if (index == 1L)
                    {
                        this.highWaterMark = GetHighWaterMark(reader);
                        count = this.highWaterMark - lastOldArticleNumber;
                    }

                    if (!reader.ReadToFollowing(ArticleElementName))
                    {
                        return entries.Values;
                    }

                    while ((entries.Count < count) &&
                        (reader.NodeType == XmlNodeType.Element) && (reader.Name == ArticleElementName))
                    {
                        var entry = GetEntry(reader);
                        entries[entry.MessageId] = entry;
                    }
                }

                index += MaxResults;
            }

            return entries.Values;
        }

        private static long GetHighWaterMark(XmlReader reader)
        {
            reader.ReadToFollowing("openSearch:totalResults");
            return reader.ReadElementContentAsLong();
        }

        [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "All ASCII, normalization problems will not occur.")]
        private static Entry GetEntry(XmlReader reader)
        {
            Entry result = new Entry();
            reader.ReadStartElement(ArticleElementName);

            while (reader.NodeType == XmlNodeType.Element)
            {
                switch (reader.Name.ToLowerInvariant())
                {
                    case "published":
                        result.Date = reader.ReadElementContentAsDateTime();
                        break;
                    case "author":
                        if (reader.ReadToDescendant("name"))
                        {
                            result.From = reader.ReadElementContentAsString();

                            if (reader.NodeType != XmlNodeType.EndElement)
                            {
                                // Due to the random string the sibling is not found and the reader is advanced to
                                // the end tag of the parent.
                                reader.ReadToNextSibling("sdghflasjhdflahlejk");
                            }

                            reader.ReadEndElement();
                        }

                        break;
                    case "title":
                        result.Subject = reader.ReadElementContentAsString();
                        break;
                    case "content":
                        result.Content = reader.ReadElementContentAsString();
                        break;
                    case "link":
                        string rel = string.Empty;
                        string href = null;

                        while (reader.MoveToNextAttribute())
                        {
                            switch (reader.Name.ToLowerInvariant())
                            {
                                case "rel":
                                    rel = reader.Value;
                                    break;
                                case "href":
                                    href = reader.Value;
                                    break;
                            }
                        }

                        switch (rel.ToLowerInvariant())
                        {
                            case "self":
                                result.MessageId = href;
                                break;
                            case "http://gdata.youtube.com/schemas/2007#in-reply-to":
                                result.References = new[] { href };
                                break;
                        }

                        reader.Skip();
                        break;
                    default:
                        reader.Skip();
                        break;
                }
            }

            reader.ReadEndElement();

            // TODO: This check should probably be moved into the SendReceiveHelper
            if ((result.Date == null) || (result.MessageId == null))
            {
                throw new IOException("Essential article header fields are missing.");
            }

            return result;
        }

        private sealed class Entry
        {
            private IList<string> references;

            ////////////////////////////////////////////////////////////////////////////////////////////////////////////

            internal string MessageId { get; set; }

            internal DateTime Date { get; set; }

            internal string From { get; set; }

            internal string Subject { get; set; }

            internal IList<string> References
            {
                get { return this.references; }
                set { this.references = value; }
            }

            [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Temporary, TODO.")]
            internal string Content { get; set; }
        }
    }
}
